Design Elevator System

Ashish

Ashish Pratap Singh

medium

In this chapter, we will explore the low-level design of an elevator system in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.

Here is an example of how a conversation between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Support multiple elevators operating within the same building
  • Handle both internal (cabin) and external (hall) floor requests
  • Assign requests to the most appropriate elevator, based on proximity and direction
  • Simulate basic elevator behaviors like moving between floors and stopping at requested floors
  • Display real-time updates for each elevator’s current floor and movement direction (UP/DOWN)

1.2 Non-Functional Requirements

  • Modularity: The system should follow object-oriented principles with clearly defined components
  • Scalability: The design should be scalable to support buildings with many elevators and floors
  • Concurrency Handling: The system must handle simultaneous floor requests without conflicts or race conditions
  • Responsiveness: The system should respond quickly to user input and reflect changes (e.g., floor updates, direction changes) in real-time
  • Maintainability: The design should be clean, modular, and easy to test, debug, or enhance

After the requirements are clear, lets identify the core entities/objects we will have in our system.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., elevator, request, floor, direction, display) and actions (e.g., move, assign, display, handle) from the functional requirements. These typically translate into classes, enums, or interfaces in an object-oriented design.

Below, we break down the functional requirements and extract the relevant entities. Related requirements are grouped together when they correspond to the same conceptual entities.

1. The system should support multiple elevators in a building.

This clearly suggests the need for an Elevator entity that encapsulates the state and behavior of an individual elevator.

We also need an ElevatorSystem (or ElevatorController) entity to manage all elevators, process incoming requests, and delegate tasks to the appropriate elevator.

2. The system must handle internal and external requests.

This implies a Request entity that captures floor requests, their origin (inside or outside the elevator), and direction.

To represent direction, we define a Direction enum with values UP and DOWN.

3. Each elevator should show its current floor and direction.

This leads to the need for an ElevatorDisplay entity. Each elevator is equipped with a display component that continuously updates and shows the elevator’s current floor and movement direction.

3. Designing Classes and Relationships

3.1 Class Definitions

Enums

Enums
  • Direction: A simple enumeration (UP, DOWN, IDLE) that defines the possible movement states of an elevator. This is crucial for both request handling and state management.
  • RequestSource: Distinguishes between requests made from inside the elevator cabin (INTERNAL) and those from a building floor (EXTERNAL). This helps in prioritizing and routing requests correctly.

Data Classes

Request

A data-centric class that encapsulates all information about a user's request.

Request

It holds the targetFloor, the desired direction (for external requests), and the source. This makes passing request information throughout the system clean and simple.

Core Classes

Elevator

This is the central active object representing a single elevator car.

Elevator

It implements Runnable to operate on its own thread, manages its internal request queues (upRequests, downRequests), and maintains its current state (IdleState, MovingUpState, etc.). It also acts as the "Subject" in the Observer pattern.

ElevatorSystem

The main controller and entry point for the entire system.

ElevatorSystem

It's a Singleton that initializes and manages all Elevator instances. It acts as a Facade, providing a simple API for external and internal requests while hiding the underlying complexity of elevator selection and request dispatching.

3.2 Class Relationships

The relationships between classes are designed to promote low coupling and high cohesion.

Composition

A "has-a" relationship where the part cannot exist without the whole

  • ElevatorSystem has an ElevatorSelectionStrategy: The strategy is an essential, integral part of the system, created and owned by it.
  • ElevatorSystem has Elevators: The system creates, manages, and contains the elevators. The elevators' lifecycle is directly controlled by the ElevatorSystem and its thread pool.
  • Elevator has an ElevatorState: Every elevator has a state object that defines its current behavior. The elevator manages the lifecycle of its state objects.

Aggregation

A "has-a" relationship where the part can exist independently

  • Elevator has ElevatorObservers: The Elevator holds a list of observers (like Display), but the observers are created externally and can exist independently of the elevator.

Implementation (An "is-a" relationship)

  • IdleState, MovingUpState, and MovingDownState implement the ElevatorState interface.
  • NearestElevatorStrategy implements the ElevatorSelectionStrategy interface.
  • Display implements the ElevatorObserver interface.
  • Elevator implements the Runnable interface, making it an active object that can run on a thread.

Association

  • Elevator processes Requests: An elevator maintains collections of requests it needs to serve.
  • ElevatorSystem receives and delegates Requests to the appropriate elevator.

3.3 Key Design Patterns

Several design patterns are employed to create a robust and flexible system architecture.

Strategy Pattern

This pattern is used to select an elevator for an external request.

ElevatorSelectionStrategy
  • Context: ElevatorSystem
  • Strategy Interface: ElevatorSelectionStrategy
  • Concrete Strategy: NearestElevatorStrategy
  • Benefit: The dispatching algorithm is decoupled from the main system. We can easily introduce new strategies (e.g., "LeastBusyElevatorStrategy") without modifying the ElevatorSystem.

State Pattern

This pattern manages the complex, state-dependent behavior of an elevator.

ElevatorState
  • Context: Elevator
  • State Interface: ElevatorState
  • Concrete States: IdleState, MovingUpState, MovingDownState
  • Benefit: It cleanly encapsulates what an elevator does in a particular state. The Elevator class becomes simpler, as it just delegates actions to the current state object. This makes the code easier to understand and extend (e.g., adding a MaintenanceState).

Observer Pattern

This pattern provides a way to notify multiple objects about state changes in an elevator.

ElevatorObserver
  • Subject: Elevator
  • Observer: ElevatorObserver (implemented by Display)
  • Benefit: It decouples the Elevator from its observers. We can attach any number of displays, loggers, or monitoring tools to an elevator without changing its code. The elevator simply notifies all registered observers when its floor or direction changes.

Facade Pattern

The ElevatorSystem acts as a facade. It provides a simple, unified interface (requestElevator(), selectFloor()) to the more complex underlying subsystem of elevators, states, strategies, and threads. This simplifies the interaction for the client (ElevatorSystemDemo).

Singleton Pattern

The ElevatorSystem class is implemented as a Singleton to ensure there is only one instance controlling the entire building's elevator network. This provides a single, global point of access and prevents conflicting states.

3.4 Full Class Diagram

Elevator System Class Diagram

4. Implementation

4.1 Enums: Direction and RequestSource

1class Direction(Enum):
2    UP = "UP"
3    DOWN = "DOWN"
4    IDLE = "IDLE"
5
6class RequestSource(Enum):
7    INTERNAL = "INTERNAL"  # From inside the cabin
8    EXTERNAL = "EXTERNAL"  # From the hall/floor
  • Direction defines the elevator’s movement state.
  • RequestSource distinguishes between cabin (internal) and hall (external) requests—critical for prioritizing and routing.

4.2 Request

Encapsulates all the information needed to process a user's request.

1@dataclass
2class Request:
3    target_floor: int
4    direction: Direction  # Primarily for External requests
5    source: RequestSource
6
7    def __str__(self):
8        if self.source == RequestSource.EXTERNAL:
9            return f"{self.source.value} Request to floor {self.target_floor} going {self.direction.value}"
10        else:
11            return f"{self.source.value} Request to floor {self.target_floor}"

4.3 Observer Pattern: Display

To provide real-time updates on the elevator's status without coupling the Elevator class to a specific display mechanism, we use the Observer pattern.

1class ElevatorObserver(ABC):
2    @abstractmethod
3    def update(self, elevator):
4        pass
5
6class Display(ElevatorObserver):
7    def update(self, elevator):
8        print(f"[DISPLAY] Elevator {elevator.get_id()} | Current Floor: {elevator.get_current_floor()} | Direction: {elevator.get_direction().value}")

The Elevator is the "Subject" and Display is the "Observer". The Elevator notifies all its registered observers (notifyObservers()) whenever its state (floor, direction) changes. This allows us to add any number of different observers (e.g., a logging service, a graphical UI, a maintenance monitor) without modifying the Elevator class.

4.4 Elevator Selection Strategy

To make the algorithm for assigning an external request to an elevator pluggable, we use the Strategy pattern.

1class ElevatorSelectionStrategy(ABC):
2    @abstractmethod
3    def select_elevator(self, elevators: List, request: Request) -> Optional:
4        pass
5
6class NearestElevatorStrategy(ElevatorSelectionStrategy):
7    def select_elevator(self, elevators: List, request: Request) -> Optional:
8        best_elevator = None
9        min_distance = float('inf')
10
11        for elevator in elevators:
12            if self._is_suitable(elevator, request):
13                distance = abs(elevator.get_current_floor() - request.target_floor)
14                if distance < min_distance:
15                    min_distance = distance
16                    best_elevator = elevator
17
18        return best_elevator
19
20    def _is_suitable(self, elevator, request: Request) -> bool:
21        if elevator.get_direction() == Direction.IDLE:
22            return True
23        if elevator.get_direction() == request.direction:
24            if request.direction == Direction.UP and elevator.get_current_floor() <= request.target_floor:
25                return True
26            if request.direction == Direction.DOWN and elevator.get_current_floor() >= request.target_floor:
27                return True
28        return False

ElevatorSelectionStrategy interface defines a contract for any selection algorithm. By programming to this interface, the main ElevatorSystem is decoupled from the specific implementation of the selection logic. We could easily introduce new strategies (e.g., "least busy elevator," "energy-saving elevator") without changing the core system.

NearestElevatorStrategy selects the best elevator for a hall request using the nearest moving-in-same-direction or idle heuristic.

4.5 Elevator State Machine

The behavior of an elevator (how it moves, how it accepts new requests) changes drastically depending on whether it's idle, moving up, or moving down. The State pattern is a perfect fit to manage this complexity.

ElevatorState

This interface defines the operations that depend on the elevator's state. The Elevator class will delegate calls to these methods to its current state object, effectively changing its behavior by changing its state object.

1class ElevatorState(ABC):
2    @abstractmethod
3    def move(self, elevator):
4        pass
5
6    @abstractmethod
7    def add_request(self, elevator, request: Request):
8        pass
9
10    @abstractmethod
11    def get_direction(self) -> Direction:
12        pass

Each state class encapsulates the logic for that specific state. For example, MovingUpState only concerns itself with servicing the next highest floor in its queue.

IdleState

1class IdleState(ElevatorState):
2    def move(self, elevator):
3        if elevator.get_up_requests():
4            elevator.set_state(MovingUpState())
5        elif elevator.get_down_requests():
6            elevator.set_state(MovingDownState())
7        # Else stay idle
8
9    def add_request(self, elevator, request: Request):
10        if request.target_floor > elevator.get_current_floor():
11            elevator.get_up_requests().add(request.target_floor)
12        elif request.target_floor < elevator.get_current_floor():
13            elevator.get_down_requests().add(request.target_floor)
14        # If request is for current floor, doors would open (handled implicitly by moving to that floor)
15
16    def get_direction(self) -> Direction:
17        return Direction.IDLE

MovingUpState

1class MovingUpState(ElevatorState):
2    def move(self, elevator):
3        if not elevator.get_up_requests():
4            elevator.set_state(IdleState())
5            return
6
7        next_floor = min(elevator.get_up_requests())
8        elevator.set_current_floor(elevator.get_current_floor() + 1)
9
10        if elevator.get_current_floor() == next_floor:
11            print(f"Elevator {elevator.get_id()} stopped at floor {next_floor}")
12            elevator.get_up_requests().remove(next_floor)
13
14        if not elevator.get_up_requests():
15            elevator.set_state(IdleState())
16
17    def add_request(self, elevator, request: Request):
18        # Internal requests always get added to the appropriate queue
19        if request.source == RequestSource.INTERNAL:
20            if request.target_floor > elevator.get_current_floor():
21                elevator.get_up_requests().add(request.target_floor)
22            else:
23                elevator.get_down_requests().add(request.target_floor)
24            return
25
26        # External requests
27        if request.direction == Direction.UP and request.target_floor >= elevator.get_current_floor():
28            elevator.get_up_requests().add(request.target_floor)
29        elif request.direction == Direction.DOWN:
30            elevator.get_down_requests().add(request.target_floor)
31
32    def get_direction(self) -> Direction:
33        return Direction.UP

The states are responsible for managing transitions. For instance, when MovingUpState has no more up-requests, it transitions the elevator's state to IdleState (or MovingDownState if down-requests exist). This keeps the transition logic clean and localized.

MovingDownState

1class MovingDownState(ElevatorState):
2    def move(self, elevator):
3        if not elevator.get_down_requests():
4            elevator.set_state(IdleState())
5            return
6
7        next_floor = max(elevator.get_down_requests())
8        elevator.set_current_floor(elevator.get_current_floor() - 1)
9
10        if elevator.get_current_floor() == next_floor:
11            print(f"Elevator {elevator.get_id()} stopped at floor {next_floor}")
12            elevator.get_down_requests().remove(next_floor)
13
14        if not elevator.get_down_requests():
15            elevator.set_state(IdleState())
16
17    def add_request(self, elevator, request: Request):
18        # Internal requests always get added to the appropriate queue
19        if request.source == RequestSource.INTERNAL:
20            if request.target_floor > elevator.get_current_floor():
21                elevator.get_up_requests().add(request.target_floor)
22            else:
23                elevator.get_down_requests().add(request.target_floor)
24            return
25
26        # External requests
27        if request.direction == Direction.DOWN and request.target_floor <= elevator.get_current_floor():
28            elevator.get_down_requests().add(request.target_floor)
29        elif request.direction == Direction.UP:
30            elevator.get_up_requests().add(request.target_floor)
31
32    def get_direction(self) -> Direction:
33        return Direction.DOWN

4.6 Elevator

The Elevator class brings together the State and Observer patterns. It runs in its own thread to simulate independent operation.

1class Elevator:
2    def __init__(self, elevator_id: int):
3        self.id = elevator_id
4        self.current_floor = 1
5        self.current_floor_lock = threading.Lock()
6        self.state = IdleState()
7        self.is_running = True
8        
9        self.up_requests = set()
10        self.down_requests = set()
11        
12        # Observer Pattern: List of observers
13        self.observers = []
14
15    # --- Observer Pattern Methods ---
16    def add_observer(self, observer: ElevatorObserver):
17        self.observers.append(observer)
18        observer.update(self)  # Send initial state
19
20    def notify_observers(self):
21        for observer in self.observers:
22            observer.update(self)
23
24    # --- State Pattern Methods ---
25    def set_state(self, state: ElevatorState):
26        self.state = state
27        self.notify_observers()  # Notify observers on direction change
28
29    def move(self):
30        self.state.move(self)
31
32    # --- Request Handling ---
33    def add_request(self, request: Request):
34        print(f"Elevator {self.id} processing: {request}")
35        self.state.add_request(self, request)
36
37    # --- Getters and Setters ---
38    def get_id(self) -> int:
39        return self.id
40
41    def get_current_floor(self) -> int:
42        with self.current_floor_lock:
43            return self.current_floor
44
45    def set_current_floor(self, floor: int):
46        with self.current_floor_lock:
47            self.current_floor = floor
48        self.notify_observers()  # Notify observers on floor change
49
50    def get_direction(self) -> Direction:
51        return self.state.get_direction()
52
53    def get_up_requests(self) -> Set[int]:
54        return self.up_requests
55
56    def get_down_requests(self) -> Set[int]:
57        return self.down_requests
58
59    def is_elevator_running(self) -> bool:
60        return self.is_running
61
62    def stop_elevator(self):
63        self.is_running = False
64
65    def run(self):
66        while self.is_running:
67            self.move()
68            try:
69                time.sleep(1)  # Simulate movement time
70            except KeyboardInterrupt:
71                self.is_running = False
72                break

Explanation:

  • Active Object: By implementing Runnable, each Elevator instance acts as an "active object" that runs its own lifecycle in a dedicated thread.
  • Request Queues: TreeSet is used for upRequests and downRequests. This data structure is ideal because it automatically keeps the floor numbers sorted, making it trivial to find the next floor to visit (first()). For downRequests, we provide a reverse-order comparator to ensure the highest floor number is always first.
  • Delegation to State: The run() loop and addRequest() method simply delegate the real work to the current state object. The Elevator itself doesn't need to know how to move or add a request; it just knows that its current state does.
  • Thread Safety: currentFloor is an AtomicInteger to ensure visibility and atomicity of floor updates across threads. addRequest is synchronized to prevent race conditions when multiple threads (e.g., the main system thread and the elevator's own thread) try to modify the request sets.

4.7 ElevatorSystem (Singleton + Facade)

This class acts as the central coordinator and public-facing API (Facade) for the entire system.

1class ElevatorSystem:
2    _instance = None
3    _lock = threading.Lock()
4
5    def __new__(cls, num_elevators: int):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance._initialized = False
11        return cls._instance
12
13    def __init__(self, num_elevators: int):
14        if self._initialized:
15            return
16        
17        self.selection_strategy = NearestElevatorStrategy()
18        self.executor_service = ThreadPoolExecutor(max_workers=num_elevators)
19        
20        elevator_list = []
21        display = Display()  # Create the observer
22
23        for i in range(1, num_elevators + 1):
24            elevator = Elevator(i)
25            elevator.add_observer(display)  # Attach the observer
26            elevator_list.append(elevator)
27
28        self.elevators = {elevator.get_id(): elevator for elevator in elevator_list}
29        self._initialized = True
30
31    @classmethod
32    def get_instance(cls, num_elevators: int):
33        return cls(num_elevators)
34
35    def start(self):
36        for elevator in self.elevators.values():
37            self.executor_service.submit(elevator.run)
38
39    # --- Facade Methods ---
40
41    # EXTERNAL Request (Hall Call)
42    def request_elevator(self, floor: int, direction: Direction):
43        print(f"\n>> EXTERNAL Request: User at floor {floor} wants to go {direction.value}")
44        request = Request(floor, direction, RequestSource.EXTERNAL)
45
46        # Use strategy to find the best elevator
47        selected_elevator = self.selection_strategy.select_elevator(list(self.elevators.values()), request)
48
49        if selected_elevator:
50            selected_elevator.add_request(request)
51        else:
52            print("System busy, please wait.")
53
54    # INTERNAL Request (Cabin Call)
55    def select_floor(self, elevator_id: int, destination_floor: int):
56        print(f"\n>> INTERNAL Request: User in Elevator {elevator_id} selected floor {destination_floor}")
57        request = Request(destination_floor, Direction.IDLE, RequestSource.INTERNAL)
58
59        elevator = self.elevators.get(elevator_id)
60        if elevator:
61            elevator.add_request(request)
62        else:
63            print("Invalid elevator ID.", file=sys.stderr)
64
65    def shutdown(self):
66        print("Shutting down elevator system...")
67        for elevator in self.elevators.values():
68            elevator.stop_elevator()
69        self.executor_service.shutdown()

The class is a Singleton to ensure there's only one control system. It provides a simple, clean API (requestElevator, selectFloor) that hides the internal complexity of strategies, states, and threads.

It uses a FixedThreadPool to manage the Elevator threads. This provides a bounded resource pool for the active elevator objects.

4.8 ElevatorSystemDemo

The main method demonstrates the end-to-end flow of the system, simulating user interactions and showing how the different components work together.

1class ElevatorSystemDemo:
2    @staticmethod
3    def main():
4        import sys
5        
6        # Setup: A building with 2 elevators
7        num_elevators = 2
8        # The get_instance method now initializes the elevators and attaches the Display (Observer).
9        elevator_system = ElevatorSystem.get_instance(num_elevators)
10
11        # Start the elevator system
12        elevator_system.start()
13        print("Elevator system started. ConsoleDisplay is observing.\n")
14
15        # --- SIMULATION START ---
16
17        # 1. External Request: User at floor 5 wants to go UP.
18        # The system will dispatch this to the nearest elevator (likely E1 or E2, both at floor 1).
19        elevator_system.request_elevator(5, Direction.UP)
20        time.sleep(0.1)  # Wait for the elevator to start moving
21
22        # 2. Internal Request: Assume E1 took the previous request.
23        # The user gets in at floor 5 and presses 10.
24        # We send this request directly to E1.
25
26        # Note: In a real simulation, we'd wait until E1 reaches floor 5, but for this demo,
27        # we simulate the internal button press shortly after the external one.
28        elevator_system.select_floor(1, 10)
29        time.sleep(0.2)
30
31        # 3. External Request: User at floor 3 wants to go DOWN.
32        # E2 (likely still idle at floor 1) might take this, or E1 if it's convenient.
33        elevator_system.request_elevator(3, Direction.DOWN)
34        time.sleep(0.3)
35
36        # 4. Internal Request: User in E2 presses 1.
37        elevator_system.select_floor(2, 1)
38
39        # Let the simulation run for a while to observe the display updates
40        print("\n--- Letting simulation run for 1 second ---")
41        time.sleep(1)
42
43        # Shutdown the system
44        elevator_system.shutdown()
45        print("\n--- SIMULATION END ---")
46
47if __name__ == "__main__":
48    ElevatorSystemDemo.main()

5. Run and Test

Languages
Java
C#
Python
C++
Files14
entities
enum
observers
states
strategy
elevator_system_demo.py
main
elevator_system.py
elevator.py
elevator_system_demo.py
Output

6. Quiz

Design Elevator System - Quiz

1 / 21
Multiple Choice

What is the main reason to introduce a 'Request' entity in the Elevator System design?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script